﻿using System;
using System.Collections.Generic;
using System.Net;

namespace Codeflows.Tracking
{
    /// <summary>
    /// Implements the state machine of a codeflow.
    /// </summary>
    public class CodeflowInstance
    {
        static readonly DateTime MinDate = new DateTime(1901, 1, 1);
        CodeflowAction _currentAction;
        CodeflowAction _lastAction;

        internal CodeflowInstance(string codeflowType, string friendlyName, string startActionName,
                                  IDictionary<string, object> arguments)
        {
            CodeflowType = codeflowType;
            FriendlyName = friendlyName;
            CreatedOnUtc = DateTime.UtcNow;
            ModifiedOnUtc = DateTime.UtcNow;
            Status = CodeflowStatus.Created;
            Host = Dns.GetHostName();
            LockedUntilUtc = MinDate;
            CurrentAction = new CodeflowAction(startActionName, arguments);
        }

        public CodeflowInstance(Guid codeflowId, string codeflowType, string friendlyName, CodeflowStatus status,
                                DateTime? timeToWakeup,
                                DateTime createdOnUtc, DateTime modifiedOnUtc, DateTime lockedUntilUtc,
                                Guid dispatcherId,
                                CodeflowAction currentAction)
        {
            CodeflowId = codeflowId;
            CodeflowType = codeflowType;
            FriendlyName = friendlyName;
            Status = status;
            TimeToWakeUp = timeToWakeup;
            CreatedOnUtc = createdOnUtc;
            ModifiedOnUtc = modifiedOnUtc;
            LockedUntilUtc = lockedUntilUtc;
            DispatcherId = dispatcherId;
            _currentAction = currentAction;
        }

        public Guid CodeflowId { get; internal set; }
        public string CodeflowType { get; private set; }
        public string FriendlyName { get; private set; }
        public DateTime? TimeToWakeUp { get; private set; }
        public DateTime CreatedOnUtc { get; private set; }
        public DateTime ModifiedOnUtc { get; private set; }
        public DateTime LockedUntilUtc { get; private set; }
        public Guid DispatcherId { get; private set; }
        public string Host { get; private set; }
        public CodeflowStatus Status { get; private set; }


        public CodeflowAction CurrentAction
        {
            get { return _currentAction; }
            set
            {
                _lastAction = _currentAction;
                _currentAction = value;
            }
        }

        public CodeflowAction LastAction
        {
            get { return _lastAction; }
        }

        internal void Own(TimeSpan timeout)
        {
            if (LockedUntilUtc >= DateTime.UtcNow)
                throw DiagnosticsUtility.CreateException("Unable to own the codeflow {0}. Instance is locked until {1}",
                                                         CodeflowId, LockedUntilUtc);

            LockedUntilUtc = DateTime.UtcNow.Add(timeout);
            DispatcherId = Guid.NewGuid();
        }

        internal void Run()
        {
            _lastAction = null;

            if (((Status == CodeflowStatus.Created || Status == CodeflowStatus.Runnable ||
                  Status == CodeflowStatus.Running))
                || (Status == CodeflowStatus.Sleeping && TimeToWakeUp < DateTime.UtcNow))
            {
                Transit(CodeflowStatus.Running);
                CurrentAction.DispatchAttemptsCount++;
            }
            else
            {
                throw DiagnosticsUtility.CreateException(
                    "Unable to run this instance. Instance is either not in a valid state or locked by another " +
                    "CodeflowApplication. Instance status is {0}. Instance is locked until {1} UTC time. " +
                    "Current action is {2}",
                    Status, LockedUntilUtc, CurrentAction.Name);
            }
        }

        internal void Jump(string nextAction, IDictionary<string, object> arguments)
        {
            EnsureRunning();
            CompleteCurrentAndTransit(CodeflowStatus.Runnable);
            CurrentAction = new CodeflowAction(nextAction, arguments);
        }

        internal void Resume(string action, IDictionary<string, object> arguments)
        {
            if (Status == CodeflowStatus.Paused && CurrentAction.Name == action)
            {
                CompleteCurrentAndTransit(CodeflowStatus.Runnable);
                CurrentAction = new CodeflowAction(action, arguments);
                return;
            }

            throw DiagnosticsUtility.CreateException("Cannot resume this codeflow.");
        }

        internal void Suspend(Exception exception)
        {
            EnsureRunning();
            CurrentAction.Fault(CodeflowStatus.Suspended, exception);
            ReleaseLock(CodeflowStatus.Suspended);
        }

        internal void Retry()
        {
            EnsureRunning();
            Transit(CodeflowStatus.Runnable);
        }

        internal void End()
        {
            EnsureRunning();
            CompleteCurrentAndReleaseLock(CodeflowStatus.Ended);
        }

        internal void Terminate()
        {
            CompleteCurrentAndReleaseLock(CodeflowStatus.Terminated);
        }

        internal void Pause(string resumeAction)
        {
            EnsureRunning();
            CompleteCurrentAndReleaseLock(CodeflowStatus.Paused);
            CurrentAction = new CodeflowAction(resumeAction);
        }

        internal void Sleep(string nextAction, IDictionary<string, object> arguments, TimeSpan duration)
        {
            EnsureRunning();
            TimeToWakeUp = DateTime.UtcNow.Add(duration);
            CompleteCurrentAndReleaseLock(CodeflowStatus.Sleeping);
            CurrentAction = new CodeflowAction(nextAction, arguments);
        }

        void CompleteCurrentAndTransit(CodeflowStatus status)
        {
            CurrentAction.Complete(status);
            Transit(status);
        }

        void CompleteCurrentAndReleaseLock(CodeflowStatus status)
        {
            CurrentAction.Complete(status);
            ReleaseLock(status);
        }

        void Transit(CodeflowStatus status)
        {
            Status = status;
            ModifiedOnUtc = DateTime.UtcNow;
        }

        void ReleaseLock(CodeflowStatus status)
        {
            LockedUntilUtc = MinDate;
            Transit(status);
        }

        void EnsureRunning()
        {
            if (Status != CodeflowStatus.Running)
                throw DiagnosticsUtility.CreateException(
                    "Codeflow with codeflow id {0} is not currently in Running state. " +
                    "Unable to complete the current action.", CodeflowId);
        }
    }
}